﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.IO.Ports;
using System.Threading;

namespace DotNetGPSTest
{
    public partial class Form1 : Form, INMEAListener, IRendererListener
    {
       enum CommsStatus
       {
          csNoConnection,
          csComPortConnected,
          csPortDataReceived
       };

       enum GPSStatus
       {
          gsNoGPS,
          gsInvalidGPS,
          gsBadValidGPS,
          gsMidValidGPS,
          gsGoodValidGPS
       };

       CommsStatus m_commsStatus = CommsStatus.csNoConnection;
       GPSStatus m_gpsStatus = GPSStatus.gsNoGPS;

        MapRenderer m_rend;

        private static NmeaInterpreter MyInterpreter = new NmeaInterpreter();
        private int MaximumDOPAllowed = 100;
        private int MidDOPAllowed = 15;
        private double CurrentHDOP;

        private static Mutex mut = new Mutex();

        static bool _continue;

        MainMenu m_savedMenu;

        string m_status = "No pos ";

        int m_zoomLevel = 8;

        bool m_maximised = false;

        static int m_bufLength = 0;
        const int m_cMaxBufLength = 256;
        const int gMaxNMEAStringLength = 128;
        static byte[] m_nmeaBuf = new byte[m_cMaxBufLength];

        int m_viewWidth = 240;
        int m_viewHeight = 180;

        static LatLongToOSGB m_coordConv = new LatLongToOSGB();
        double m_easting = 467400;
        double m_northing = 109300;
        int m_gpsEasting = -1;
        int m_gpsNorthing = -1;

        private Bitmap m_backBuffer;

        static SerialPort m_gpsPort;// = new SerialPort("COM7", 9600);

        Thread readThread = new Thread(readFromSerialPort);

        static INMEAListener m_nmeaListener;

        private NmeaInterpreter m_myInterpreter = new NmeaInterpreter();

        const bool usingHP = false;
        string mapFolder = "";

        public Form1()
        {
           

           OSTileProvider os = new OSTileProvider();
           // Getting 400096,122290
           Point p = os.toGridRef(new GPSPosition(51.0, -2.0));
           // Getting C_0401/45001120.gif
           string file = os.getBitmapFilenameAt(new Point(450001, 112001));
           //Carry on testing! Next to test is OSTileProvider::getTileTopLeftOffset
           //   And probably test more of the above - would BeginInvoke good to
           //      have auto testing like at work

           MyInterpreter.PositionReceived += OnPositionReceived;
           MyInterpreter.HDOPReceived += OnHDOPReceived;

             //m_rend = new MapRenderer(new MapSpec(500, 800,
             //     "D_", "\\Storage Card\\Maps", ".gif"), m_viewWidth, m_viewHeight);

           if(usingHP)
           {
               mapFolder = "\\Storage Card";
               m_rend = new MapRenderer(new MapSpec(200, 40000,
                            "K_", mapFolder, ".png"), m_viewWidth, m_viewHeight, 3);
           }
           else
           {
               mapFolder = "\\SD-MMCard";
               m_rend = new MapRenderer(new MapSpec(200, 40000,
                            "K_", mapFolder, ".png"), m_viewWidth, m_viewHeight, 4);
           }

           
           
           
            InitializeComponent();

            m_nmeaListener = this;

           // m_myInterpreter.PositionReceived += new System.EventHandler(onPositionReceived);
            m_rend.drawTilesAt((int)m_easting, (int)m_northing, this);

           bool comPortError = false;

           try
           {
               if(m_gpsPort != null)
                  m_gpsPort.Open();
           }
           catch(Exception)
           {
              m_commsStatus = CommsStatus.csNoConnection;
              comPortError = true;
           }
           if(!comPortError)
           {
              m_commsStatus = CommsStatus.csComPortConnected;    
           }

           this.ControlBox = false;
           _continue = true;
           readThread.Start();
        }

        public void newNMEA(string message)
        {
            MessageBox.Show(message);
        }

        public void OnPositionReceived(double latitude,
          double longitude)
        {
            // Is the HDOP at least six?
           if(CurrentHDOP > MaximumDOPAllowed)
              m_gpsStatus = GPSStatus.gsBadValidGPS;
           
           if (CurrentHDOP <= MaximumDOPAllowed)
              m_gpsStatus = GPSStatus.gsMidValidGPS;

           if (CurrentHDOP <= MidDOPAllowed)
              m_gpsStatus = GPSStatus.gsGoodValidGPS;

           m_coordConv.lati_ = latitude;
           m_coordConv.longi_ = longitude;
           m_coordConv.Convert();
           m_gpsEasting = m_coordConv.e;
           m_gpsNorthing = m_coordConv.n;

           m_status = m_gpsEasting + "," + m_gpsNorthing;

           m_commsStatus = CommsStatus.csPortDataReceived;

           //Causes flicker. Need to sort out double buffe
           draw();
        }

        private void OnHDOPReceived(double value)
        {
           if (m_commsStatus == CommsStatus.csComPortConnected)
           {
              m_commsStatus = CommsStatus.csPortDataReceived;
              draw();
           }
            // Remember the current HDOP value
            CurrentHDOP = value;
        }

        public delegate void SetWeatherCallback();

        public void draw()
        {
           if (this.InvokeRequired)
           {
              SetWeatherCallback cb = new SetWeatherCallback(draw);
              this.Invoke(cb);
           }
           else
           {
              this.Invalidate();
           }
        }

        private void buttonGo_Click(object sender, EventArgs e)
        {
           if (!m_maximised)
           {
              this.WindowState = FormWindowState.Maximized;
              m_savedMenu = this.Menu;
              this.Menu = null;
              this.ControlBox = false;
              m_maximised = true;

              this.lookAhead.Visible = false;
              this.zoomIn.Visible = false;
              this.zoomOut.Visible = false;

              this.buttonGo.Width = 15;
              this.buttonGo.Height = 15;
              this.buttonGo.Left = this.Width - 15;
              this.buttonGo.Top = 0;
           }
           else
           {
              this.WindowState = FormWindowState.Normal;
              this.Menu = m_savedMenu;
              this.ControlBox = false;
              m_maximised = false;

              this.lookAhead.Visible = true;
              this.zoomIn.Visible = true;
              this.zoomOut.Visible = true;  
           }
        }

        protected override void OnPaintBackground(PaintEventArgs pevent)
        {
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
           
            if (m_backBuffer == null)
            {
                m_backBuffer = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
            }

            Graphics g = Graphics.FromImage(m_backBuffer);
            g.Clear(Color.Pink);

            if (m_panning)
            {
               g.DrawImage(m_scrolly, m_panX, m_panY);
            }
            else
               m_rend.drawTiles(g);

            if(!m_maximised)
            {
               g.DrawLine(new Pen(Color.Black), Convert.ToInt32(m_viewWidth*0.40), m_viewHeight / 2,Convert.ToInt32(m_viewWidth * 0.6), m_viewHeight / 2);
               g.DrawLine(new Pen(Color.Black), m_viewWidth / 2, Convert.ToInt32(m_viewHeight*0.40), m_viewWidth / 2, Convert.ToInt32(m_viewHeight*0.60));

               if (m_gpsStatus == GPSStatus.gsGoodValidGPS &&
                  m_commsStatus == CommsStatus.csPortDataReceived)
               {
                  g.FillRectangle(new SolidBrush(Color.LightGreen), 0, this.Height - 5, 5, 5);
               }
               else
               {

                  switch (m_gpsStatus)
                  {
                     case GPSStatus.gsNoGPS:
                        g.FillRectangle(new SolidBrush(Color.Black), 0, this.Height - 3, 3, 3);
                        this.lookAhead.Enabled = true;
                        break;
                     case GPSStatus.gsInvalidGPS:
                        g.FillRectangle(new SolidBrush(Color.Red), 0, this.Height - 3, 3, 3);
                        g.FillRectangle(new SolidBrush(Color.Red), 3, this.Height - 6, 3, 6);
                        this.lookAhead.Enabled = true;
                        break;
                     case GPSStatus.gsBadValidGPS:
                        g.FillRectangle(new SolidBrush(Color.DarkGreen), 0, this.Height - 3, 3, 3);
                        g.FillRectangle(new SolidBrush(Color.DarkGreen), 3, this.Height - 6, 3, 6);
                        g.FillRectangle(new SolidBrush(Color.DarkGreen), 6, this.Height - 9, 3, 9);
                        this.lookAhead.Enabled = true;
                        break;
                     case GPSStatus.gsMidValidGPS:
                        g.FillRectangle(new SolidBrush(Color.Green), 0, this.Height - 3, 3, 3);
                        g.FillRectangle(new SolidBrush(Color.Green), 3, this.Height - 6, 3, 6);
                        g.FillRectangle(new SolidBrush(Color.Green), 6, this.Height - 9, 3, 9);
                        g.FillRectangle(new SolidBrush(Color.Green), 9, this.Height - 12, 3, 12);
                        this.lookAhead.Enabled = true;
                        break;
                     case GPSStatus.gsGoodValidGPS:
                        g.FillRectangle(new SolidBrush(Color.LightGreen), 0, this.Height - 3, 3, 3);
                        g.FillRectangle(new SolidBrush(Color.LightGreen), 3, this.Height - 6, 3, 6);
                        g.FillRectangle(new SolidBrush(Color.LightGreen), 6, this.Height - 9, 3, 9);
                        g.FillRectangle(new SolidBrush(Color.LightGreen), 9, this.Height - 12, 3, 12);
                        g.FillRectangle(new SolidBrush(Color.LightGreen), 12, this.Height - 15, 3, 15);
                        this.lookAhead.Enabled = true;
                        break;
                     default:
                        break;
                  }
                  switch (m_commsStatus)
                  {
                      case CommsStatus.csNoConnection:
                        g.FillEllipse(new SolidBrush(Color.Red), 15, this.Height - 15, 15, 15);
                        break;
                     case CommsStatus.csComPortConnected:
                        g.FillEllipse(new SolidBrush(Color.Orange), 15, this.Height - 15, 15, 15);
                        break;
                     case CommsStatus.csPortDataReceived:
                        g.FillEllipse(new SolidBrush(Color.LightGreen), 15, this.Height - 10, 10, 10);
                        break;
                     default:
                        break;
                  }
               }
            g.FillRectangle(new SolidBrush(Color.White), 32
                , this.Height - 25, 375, 25);
            g.DrawString(
                m_status, new Font("Tahoma", 8, FontStyle.Regular),
                new SolidBrush(Color.Black),
                34, this.Height - 25);

            g.DrawString(
                m_easting.ToString() + "," + m_northing.ToString(), new Font("Tahoma", 8, FontStyle.Regular),
                new SolidBrush(Color.Black),
                225, this.Height - 25);
        }
            g.Dispose();
            e.Graphics.Clear(Color.Pink);
            e.Graphics.DrawImage(m_backBuffer, 0, 0);
        }

        public static void readFromSerialPort()
        {
            int bytesToRead = 0;

            if (m_gpsPort == null)
                return;

            //clearSerialPort();

            while (_continue)
            {
               if (m_gpsPort.IsOpen)
               {
                  Thread.Sleep(10);
                  try
                  {
                     bytesToRead = m_gpsPort.BytesToRead;
                     if (m_bufLength + bytesToRead < m_cMaxBufLength)
                     {
                        m_gpsPort.Read(m_nmeaBuf, m_bufLength, bytesToRead);
                        m_bufLength += bytesToRead;

                        processNMEABuffer();
                     }
                     else
                     {
                        clearSerialPort();
                     }
                  }
                  catch (TimeoutException) { }
                  catch (BufferOverflowException)
                  { MessageBox.Show("BufferOverflowException" + bytesToRead.ToString()); }
               }
           }
        }

        public static void clearSerialPort()
        {
            if (m_gpsPort == null)
                return;

            int bytesToRead = 0;
            byte[] clearBuf = new byte[m_cMaxBufLength];

            bytesToRead = m_gpsPort.BytesToRead;
            while (bytesToRead > 0)
            { 
                m_gpsPort.Read(clearBuf, 0, m_cMaxBufLength);
                bytesToRead = m_gpsPort.BytesToRead;
            }
            m_bufLength = 0;
           
        }

        public static void processNMEABuffer()
        {
            bool lookingForNMEA = true;
            while(lookingForNMEA)
            {
               byte[] nmeaStringBuffer = new byte[gMaxNMEAStringLength];
               string nmea = "";
               lookingForNMEA = false;

               bool foundNMEAStart = findStartOfNMEA();
               if(foundNMEAStart)
               {
                  int endOfNMEA = findEndOfNMEA();
                  if(endOfNMEA != 0)
                  {
                     // Found NMEA
                     if(endOfNMEA < gMaxNMEAStringLength) // If greater than this then can't be NMEA? What's max NMEA string length?
                     {
                        // We've found a NMEA string
                        memcpy(nmeaStringBuffer, m_nmeaBuf, endOfNMEA, 0);

                        // Clear old string by replacing $ with \n
                        m_nmeaBuf[0] = (byte)'\n';

                        nmeaStringBuffer[endOfNMEA] = 0;

                        for (int i = 0; i < endOfNMEA; i++)
                           nmea += (char)nmeaStringBuffer[i];

                        MyInterpreter.Parse(nmea);

                        clearSerialPort();
                        
                        lookingForNMEA = true;

                     }
                     else
                     {
                        // Buf too small error
                     }
                  }
               }
            }
        }

        public static bool findStartOfNMEA()
        {
           byte[] tempBuffer = new byte[m_cMaxBufLength];

           for (int i = 0; i < m_bufLength; i++)
           {
              if (m_nmeaBuf[i] == '$')
              {
                 //Shift '$' to start of buffer
                 memcpy(tempBuffer, m_nmeaBuf, m_bufLength - i, i);
                 memcpy(m_nmeaBuf, tempBuffer, m_bufLength - i, 0);
                 m_bufLength -= i;

                 return true;
              }
           }
           return false;
        }

        public static int findEndOfNMEA()
        {
           for (int i = 0; i < m_bufLength; i++)
           {
              if (m_nmeaBuf[i] == '\r')
              {
                 if (i + 1 < m_bufLength)
                 {
                    if (m_nmeaBuf[i + 1] == '\n')
                    {
                       return i;
                    }
                 }
              }
           }
           return 0;
        }

        public static void memcpy(byte[] dest, byte[] src, int len, int srcOffset)
        {
           for (int i = 0; i < len; i++)
           {
              dest[i] = src[srcOffset + i];
           }
        }

        private void Form1_Resize(object sender, EventArgs e)
        {
              m_backBuffer = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
              m_viewWidth = ClientSize.Width;
              m_viewHeight = ClientSize.Height;
              m_rend.m_viewWidth = ClientSize.Width;
              m_rend.m_viewHeight = ClientSize.Height;
              m_rend.drawTilesAt((int)m_easting, (int)m_northing, this);

              positionControls();
        }

        private void zoomIn_Click(object sender, EventArgs e)
        {
            zoomInMap();
        }

        private void zoomInMap()
        {
            if (m_zoomLevel > 1)
            {
                m_zoomLevel--;

                switch (m_zoomLevel)
                {
                    case 1:
                        m_rend.setMapSpec(new MapSpec(200, 500,
                            "D_", mapFolder, ".png"));
                        break;
                    case 2:
                        m_rend.setMapSpec(new MapSpec(200, 1000,
                            "E_", mapFolder, ".png"));
                        break;
                    case 3:
                        m_rend.setMapSpec(new MapSpec(200, 1000,
                            "F_", mapFolder, ".png"));
                        break;
                    case 4:
                        m_rend.setMapSpec(new MapSpec(200, 2000,
                            "G_", mapFolder, ".png"));
                        break;
                    case 5:
                        m_rend.setMapSpec(new MapSpec(200, 5000,
                            "H_", mapFolder, ".png"));
                        break;
                    case 6:
                        m_rend.setMapSpec(new MapSpec(200, 10000,
                            "I_", mapFolder, ".png"));
                        break;
                    case 7:
                        m_rend.setMapSpec(new MapSpec(200, 20000,
                           "J_", mapFolder, ".png"));
                        break;
                    case 8:
                        m_rend.setMapSpec(new MapSpec(200, 40000,
                           "K_", mapFolder, ".png"));
                        break;

                }

                m_rend.drawTilesAt((int)m_easting, (int)m_northing, this);
            }
        }

        private void zoomOut_Click(object sender, EventArgs e)
        {
           if (m_zoomLevel < 8)
           {
              m_zoomLevel++;

              switch (m_zoomLevel)
              {
                  case 1:
                      m_rend.setMapSpec(new MapSpec(200, 500,
                          "D_", mapFolder, ".png"));
                      break;
                  case 2:
                      m_rend.setMapSpec(new MapSpec(200, 1000,
                          "E_", mapFolder, ".png"));
                      break;
                  case 3:
                      m_rend.setMapSpec(new MapSpec(200, 1000,
                          "F_", mapFolder, ".png"));
                      break;
                  case 4:
                      m_rend.setMapSpec(new MapSpec(200, 2000,
                          "G_", mapFolder, ".png"));
                      break;
                  case 5:
                      m_rend.setMapSpec(new MapSpec(200, 5000,
                          "H_", mapFolder, ".png"));
                      break;
                  case 6:
                      m_rend.setMapSpec(new MapSpec(200, 10000,
                          "I_", mapFolder, ".png"));
                      break;
                  case 7:
                      m_rend.setMapSpec(new MapSpec(200, 20000,
                         "J_", mapFolder, ".png"));
                      break;
                  case 8:
                      m_rend.setMapSpec(new MapSpec(200, 40000,
                         "K_", mapFolder, ".png"));
                      break;

              }
              m_rend.drawTilesAt((int)m_easting, (int)m_northing, this);
           }
        }

        private void lookAhead_Click(object sender, EventArgs e)
        {
           if (m_gpsNorthing >= 0 && m_gpsEasting >= 0)
           {
              m_easting = m_gpsEasting;
              m_northing = m_gpsNorthing;
              m_rend.drawTilesAt((int)m_easting, (int)m_northing, this);
           }
        }

        private void update_Click(object sender, EventArgs e)
        {
           Invalidate();
        }

        private void menuItemExit_Click(object sender, EventArgs e)
        {
           Close();
        }

        private void Form1_Closing(object sender, CancelEventArgs e)
        {
           readThread.Abort();
           readThread.Join();
            if(m_gpsPort != null)
               m_gpsPort.Close();
        }

        private void Form1_Closed(object sender, EventArgs e)
        {

        }

        public void positionControls()
        {
           this.zoomIn.Width = 32;
           this.zoomIn.Height = 32;
           this.zoomIn.Left = this.Width - 32;
           this.zoomIn.Top = 0;

           this.zoomOut.Width = 32;
           this.zoomOut.Height = 32;
           this.zoomOut.Left = this.Width - 32;
           this.zoomOut.Top = 32 + 15;

           this.buttonGo.Width = 32;
           this.buttonGo.Height = 32;
           this.buttonGo.Left = this.Width - 32;
           this.buttonGo.Top = 32 + 15 + 32 + 15;

           this.lookAhead.Width = 32;
           this.lookAhead.Height = 32;
           this.lookAhead.Left = this.Width - 32;
           this.lookAhead.Top = 32 + 15 + 32 + 15 + 32 + 15;
        }

        private void menuItem4_Click(object sender, EventArgs e)
        {

        }

        private void menuItemCOM7_Click(object sender, EventArgs e)
        {
            openNewComPort("COM7");
        }

        private void menuItemCOM4_Click(object sender, EventArgs e)
        {
            openNewComPort("COM4");
        }

        private void openNewComPort(string port)
        {
            if (m_gpsPort != null)
            {
                readThread.Abort();
                readThread.Join();
                m_gpsPort.Close();

                if (m_gpsPort.IsOpen)
                    m_gpsPort.Close();
            }

            m_commsStatus = CommsStatus.csNoConnection;

            m_gpsPort = new SerialPort(port, 9600);

            bool comPortError = false;

            try
            {
                m_gpsPort.Open();
            }
            catch (Exception)
            {
                m_commsStatus = CommsStatus.csNoConnection;
                comPortError = true;
            }
            if (!comPortError)
            {
                m_commsStatus = CommsStatus.csComPortConnected;
            }

            readThread = new Thread(readFromSerialPort);
            readThread.Start();

            draw();
        }

        Bitmap m_scrolly;
        bool m_panning = false;
        int m_mouseDownX;
        int m_mouseDownY;
        int m_panX;
        int m_panY;

        private void Form1_MouseDown(object sender, MouseEventArgs e)
        {
           m_mouseDownX = e.X;
           m_mouseDownY = e.Y;
           m_panX = 0;
           m_panY = 0;
            m_scrolly = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
           

           Graphics gScrolly = Graphics.FromImage(m_scrolly);
           gScrolly.DrawImage(m_backBuffer, 0, 0);
           m_panning = true;
           
        }

        private void Form1_MouseMove(object sender, MouseEventArgs e)
        {
           m_panX = e.X - m_mouseDownX;
           m_panY = e.Y - m_mouseDownY;
           Invalidate();
        }

        private void Form1_MouseUp(object sender, MouseEventArgs e)
        {
           m_panning = false;
           //return;

           //double dx = (e.X) - m_viewWidth / 2.0;
           //double dy = (e.Y) - m_viewHeight / 2.0;
           
           double dxM = (-m_panX) * m_rend.mpp();
           double dyM = (-m_panY) * m_rend.mpp();
           m_easting += dxM;
           m_northing -= dyM;

           m_rend.drawTilesAt((int)m_easting, (int)m_northing, this);
        }

        private void menuItemCOM8_Click(object sender, EventArgs e)
        {
            openNewComPort("COM8");
        }

        private void menuItemGPSNone_Click(object sender, EventArgs e)
        {
            readThread.Abort();
            readThread.Join();

            m_commsStatus = CommsStatus.csNoConnection;

            if (m_gpsPort != null)
            {
                if (m_gpsPort.IsOpen)
                    m_gpsPort.Close();
            }

            draw();
        }

        private void Form1_DoubleClick(object sender, EventArgs e)
        {
            zoomInMap();
        }

    }

    public class LatLongToOSGB
    {
       private const double scalechange = (20.4894 / 1000000.0);
       private const double rz = (-0.8421);
       private const double ry = (-0.247);
       private const double rx = (-0.1502);

       private const double r0c0 = (1 + scalechange);
       private const double r0c1 = (-1 * rz * 0.000004848136811);
       private const double r0c2 = (ry * 0.000004848136811);

       private const double r1c0 = (rz * 0.000004848136811);
       private const double r1c1 = r0c0;
       private const double r1c2 = (-1 * rx * 0.000004848136811);

       private const double r2c0 = (-1 * ry * 0.000004848136811);
       private const double r2c1 = (rx * 0.000004848136811);
       private const double r2c2 = r0c0;

       private const double pi = 3.1415926535;
       private const double NGa = 6377563.396;
       private const double NGb = 6356256.910;
       private const double WGS84a = 6378137.0;
       private const double WGS84b = 6356752.3141;

       private const double NGFo = 0.9996012717;
       private const double NGphiOrigin = (49.0 * pi / 180);
       private const double NGlamdaOrigin = (-2.0 * pi / 180);
       private const double NGeOrigin = 400000.0;
       private const double NGnOrigin = -100000.0;

       private const double m_aSquared = (NGa * NGa);
       private const double m_bSquared = (NGb * NGb);
       private const double m_eSquared = ((m_aSquared - m_bSquared) / m_aSquared);
       private const double m_n = ((NGa - NGb) / (NGa + NGb));
       private const double m_nToTheTwo = (m_n * m_n);
       private const double m_nToTheThree = (m_nToTheTwo * m_n);
       private const double m_NGaNGFo = (NGa * NGFo);
       private const double m_NGbNGFo = (NGb * NGFo);
       private const double m_WGS84aSquared = (WGS84a * WGS84a);
       private const double m_WGS84bSquared = (WGS84b * WGS84b);
       private const double m_WGS84eSquared = ((m_WGS84aSquared - m_WGS84bSquared) / m_WGS84aSquared);

       private double m_Northing;
       private double m_Easting;

       public double lati_;
       public double longi_;
       public string result_;
       public int e;
       public int n;

       public LatLongToOSGB()
       {
       }

       private void InputValidation()
       {
          // some validation
          if (lati_ < 30.0)
             throw new FormatException("Too far South!");

          if (lati_ > 70.0)
             throw new FormatException("Too far North!");

          if (longi_ < -10.0)
             throw new FormatException("Too far West!");

          if (longi_ > 3.0)
             throw new FormatException("Too far East!");
       }

       private void ValidateOSGB()
       {
          // some validation
          if (m_Northing < 0.0)
             throw new FormatException("Too far South!");

          if (m_Northing > 1300000.0)
             throw new FormatException("Too far North!");

          if (m_Easting < 0.0)
             throw new FormatException("Too far West!");

          if (m_Easting > 700000.0)
             throw new FormatException("Too far East!");
       }

       private void HelTrans(ref double phi, ref double lamda, ref double ElipsoidHeight)
       {
          // Helmert Transformation

          // lat long height

          //This lat long to 3-d uses WGS84 m_eSquaredIN, NGaIN

          double v = WGS84a / Math.Sqrt(1 - m_WGS84eSquared * Math.Pow(Math.Sin(phi), 2));
          double inCoOrdX = (v + ElipsoidHeight) * Math.Cos(phi) * Math.Cos(lamda);
          double inCoOrdY = (v + ElipsoidHeight) * Math.Cos(phi) * Math.Sin(lamda);
          double inCoOrdZ = ((1 - m_WGS84eSquared) * v + ElipsoidHeight) * Math.Sin(phi);

          double prod1 = (1 + scalechange) * inCoOrdX +
              (-1 * rz * 0.000004848136811) * inCoOrdY +
              (ry * 0.000004848136811) * inCoOrdZ +
              -446.448;

          double prod2 = (rz * 0.000004848136811) * inCoOrdX +
              (1 + scalechange) * inCoOrdY +
              (-1 * rx * 0.000004848136811) * inCoOrdZ +
              125.157;

          double prod3 = (-1 * ry * 0.000004848136811) * inCoOrdX +
              (rx * 0.000004848136811) * inCoOrdY +
              (1 + scalechange) * inCoOrdZ +
              -542.06;

          // This 3-d to lat long uses OSGB m_eSquared, NGa

          double p = Math.Sqrt(prod1 * prod1 + prod2 * prod2);
          double lat1 = Math.Atan2(prod3, p * (1 - m_eSquared));
          double v1 = NGa / Math.Sqrt(1 - m_eSquared * Math.Pow(Math.Sin(lat1), 2));
          double lat2 = Math.Atan2(prod3 + m_eSquared * v1 * Math.Sin(lat1), p);
          double v2 = NGa / Math.Sqrt(1 - m_eSquared * Math.Pow(Math.Sin(lat2), 2));

          phi = Math.Atan2(prod3 + m_eSquared * v2 * Math.Sin(lat2), p);
          lamda = Math.Atan2(prod2, prod1);
          ElipsoidHeight = p / Math.Cos(phi) - v2;
       }

       private void DoCalc(bool bApplyOSGB1936Transform, double phi, double lamda, double ElipsoidHeight)
       {
          if (bApplyOSGB1936Transform)
             HelTrans(ref phi, ref lamda, ref ElipsoidHeight);

          double phiSine = Math.Sin(phi);
          double phiSineSquared = phiSine * phiSine;
          double phiCos = Math.Cos(phi);
          double phiCosCubed = phiCos * phiCos * phiCos;
          double phiCosToTheFifth = phiCos * phiCos * phiCosCubed; // ****************************** FIXED
          double phiTan = Math.Tan(phi);
          double phiTanSquared = phiTan * phiTan;
          double phiTanToTheFourth = phiTanSquared * phiTanSquared;
          double OneMinuseSquaredphiSineSquared = 1.0 - m_eSquared * phiSineSquared;
          double phiMinusNGphiOrigin = phi - NGphiOrigin;
          double phiPlusNGphiOrigin = phi + NGphiOrigin;

          double v = m_NGaNGFo / Math.Sqrt(OneMinuseSquaredphiSineSquared);
          double p = m_NGaNGFo * (1.0 - m_eSquared) * Math.Pow((OneMinuseSquaredphiSineSquared), -1.5);
          double nnSquared = v / p - 1.0;

          double frag1 = (1.0 + m_n + (5.0 / 4.0) * (m_nToTheTwo + m_nToTheThree)) * phiMinusNGphiOrigin;
          double frag2 = (3.0 * (m_n + m_nToTheTwo) + (21.0 / 8.0) * m_nToTheThree) * Math.Sin(phiMinusNGphiOrigin) * Math.Cos(phiPlusNGphiOrigin);
          double frag3 = ((15.0 / 8.0) * (m_nToTheTwo + m_nToTheThree)) * Math.Sin(2.0 * (phiMinusNGphiOrigin)) * Math.Cos(2.0 * (phiPlusNGphiOrigin));
          double frag4 = ((35.0 / 24) * m_nToTheThree) * Math.Sin(3.0 * (phiMinusNGphiOrigin)) * Math.Cos(3.0 * (phiPlusNGphiOrigin));

          double lamdaDiff = lamda - NGlamdaOrigin;
          double lamdaDiffSquared = lamdaDiff * lamdaDiff;
          double lamdaDiffToTheFourth = lamdaDiffSquared * lamdaDiffSquared;

          {
             double I = m_NGbNGFo * (frag1 - frag2 + frag3 - frag4) + NGnOrigin;
             double II = (v / 2.0) * phiSine * phiCos;
             double III = (v / 24.0) * phiSine * phiCosCubed * (5 - phiTanSquared + 9 * nnSquared);
             double IIIA = (v / 720.0) * phiSine * phiCosToTheFifth * (61 - 58 * phiTanSquared + phiTanToTheFourth);

             m_Northing = I + II * lamdaDiffSquared + III * lamdaDiffToTheFourth + IIIA * lamdaDiffToTheFourth * lamdaDiffSquared;
          }
          {
             double IV = v * phiCos;
             double V = (v / 6.0) * phiCosCubed * (v / p - phiTanSquared);
             double VI = (v / 120.0) * phiCosToTheFifth * (5 - 18 * phiTanSquared + phiTanToTheFourth + 14 * nnSquared - (58 * (phiTanSquared * nnSquared)));

             m_Easting = NGeOrigin + IV * lamdaDiff + V * lamdaDiffSquared * lamdaDiff + VI * lamdaDiffToTheFourth * lamdaDiff;
          }
       }

       private void BuildOSGBString()
       {
          int Northing = (int)(m_Northing + 0.5);
          int Easting = (int)(m_Easting + 0.5);

          int osgbEasting = Easting / 100000;
          int osgbNorthing = 14 - (Northing / 100000);

          int iNorth = ((osgbNorthing * 5) / 25) * 5 + 'H';
          if (osgbEasting > 4)
             iNorth++;

          if (iNorth >= 'I')
             iNorth++;

          int iEast = (osgbEasting % 5) + 'A';
          iEast += (osgbNorthing * 5) % 25;
          if (iEast >= 'I')
             iEast++;

          result_ = "";
          result_ += (char)iNorth;
          result_ += (char)iEast;
          result_ += (Easting % 100000).ToString("D5");
          result_ += (Northing % 100000).ToString("D5");
          e = Easting;
          n = Northing;
       }

       public void Convert()
       {
          InputValidation();

          double phi = lati_ * pi / 180.0;
          double lamda = longi_ * pi / 180.0;
          DoCalc(true, phi, lamda, 0.0);
          ValidateOSGB();
          BuildOSGBString();
       }

       
    }

    class BufferOverflowException : System.Exception { }

    interface INMEAListener
    {
        void newNMEA(string message);
    }

    interface IRendererListener
    {
        void draw();
    }
}